﻿package com.janisRadins.asDraw {
	import com.janisRadins.asDraw.vo.CurveData;
	import flash.display.GraphicsPath;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.geom.Point;
	
	/**
	 * Event dispathed as any of sub curve properties have been changed.<br>
	 * @eventType com.janisRadins.asDraw.Path.DATA_CHANGED
	 */
	[Event(name = "dataChanged", type = "flash.events.Event")]
	
	/**
	 * Event dispathed as any change in collection of curves - new curves added/removed, curve index change has taken place.<br>
	 * @eventType com.janisRadins.asDraw.Path.COLLECTION_CHANGED
	 */
	[Event(name = "collectionChanged", type = "flash.events.Event")]
	
	/**
	 * Class which represents collection of curves that form continuous path
	 * @author Jānis Radiņš
	 */
	public class Path extends EventDispatcher implements ICurve, ICurveCollection {
		/**
		 * dataChanged event constant
		 * @eventType dataChanged
		 */
		public const DATA_CHANGED:String = "dataChanged";
		/**
		 * collectionChanged event constant
		 * @eventType collectionChanged
		 */
		public const COLLECTION_CHANGED:String = "collectionChanged";
		/**
		 * Out of range error ID
		 */
		public const ERROR_OOR:uint = 1;
		/**
		 * Curve not found error ID
		 */
		public const ERROR_CNF:uint = 2;
		/**
		 * Inner variable which contains list of all curves in current collection
		 */
		protected var curveList:Vector.<Curve> = null;
		/** @private */
		protected var _length:Number = NaN;
		/** @private */
		protected var _x:Number = 0;
		/** @private */
		protected var _y:Number = 0;
		/** @private */
		protected var _curveIndex:uint = 0;
		/**
		 * Constructor
		 * @param	curves Vector with curves to be used in this Path
		 */
		public function Path(curves:Vector.<Curve> = null) {
			super();
			if (curves != null) {
				curveList = curves;
				for each(var curve:Curve in curves) {
					curve.addEventListener(CurveChangeEvent.CHANGE, onCurveValueChange);
				}
			} else {
				curveList = new Vector.<Curve>();
			}
		}
		/**
		 * Inner method which fires each time as any of sub curves are changed
		 * @param	e
		 * @private
		 */
		//FIXED: Add event distpatch that curve length have changed in here
		protected function onCurveValueChange(e:CurveChangeEvent):void {
			_length = NaN;
			dispatchEvent(new Event(DATA_CHANGED));
		}
		/**
		 * Path starting point X coordinate
		 */
		public function get x():Number {
			return _x;
		}
		public function set x(value:Number):void {
			_x = value;
		}
		/**
		 * Path starting point Y coordinate
		 */
		public function get y():Number { 
			return _y;
		}
		public function set y(value:Number):void {
			_y = value;
		}
		/**
		 * Last point of path
		 */
		public function get pathClosePoint():Point {
			if (curveList.length == 0) {
				return new Point(_x, _y);
			}
			var lastCurveData:CurveData = getCurveData(getCurveAt(-1));
			var curve:Curve = lastCurveData.curve as Curve;
			return lastCurveData.coordinates.add(new Point(curve.anchorX, curve.anchorY));
		}
		/**
		 * @copy com.janisRadins.asDraw#length
		 */
		public function get length():Number {
			if (isNaN(_length) == true) {
				updateLength();
			}
			return _length;
		}
		/**
		 * Method that update total CurveCollection length value. This procedure is ran only as 
		 * CurveCollection curve list is changed or updated and only in case if length property is requested.
		 */
		private function updateLength():void {
			_length = 0;
			for each(var curve:Curve in curveList) {
				_length += curve.length;
			}
		}
		/**
		 * Method that adds stright line to Path
		 * @param	x	X coordinate of line ending relative to Path last point
		 * @param	y	X coordinate of line ending relative to Path last point
		 * @return New length of curve collection in this Path
		 */
		public function lineTo(x:Number = 0, y:Number = 0):uint {
			curveList.push(new Curve(NaN, NaN, x, y));
			dispatchEvent(new Event(COLLECTION_CHANGED));
			return curveList.length;
		}
		/**
		 * Method that adds new curve to Path
		 * @param	controlX	Controll point X value relative to Path last point
		 * @param	controlY	Controll point Y value relative to Path last point
		 * @param	anchorX		Anchor point X value relative to Path last point
		 * @param	anchorY		Anchor point Y value relative to Path last point
		 * @return New length of curve collection in this Path
		 */
		public function curveTo(controlX:Number = NaN, controlY:Number = NaN, anchorX:Number = 0, anchorY:Number = 0):uint {
			curveList.push(new Curve(controlX, controlY, anchorX, anchorY));
			dispatchEvent(new Event(COLLECTION_CHANGED));
			return curveList.length;
		}
		/**
		 * Inner method for calculating one way curve subsegment
		 * @param	start	Start length of sub curve
		 * @param	end		End length of sub curve
		 * @return GraphicsPath instance which contains sub curve data
		 * @private
		 */
		protected function getSubSegment(start:Number, end:Number):GraphicsPath {
			var startCurve:CurveData = getCurveOnPosition(start);
			var startPoint:Point = new Point(startCurve.coordinates.x, startCurve.coordinates.y);
			var pathData:GraphicsPath = new GraphicsPath();
			pathData.moveTo(startPoint.x, startPoint.y);
			var segLength:Number = end - start;
			var curve:Curve;
			var s:Number = Math.max(start - startCurve.startLength, 0);
			var e:Number;
			var subPath:GraphicsPath;
			for (var i:uint = startCurve.index; i < curveList.length; i++) {
				curve = curveList[i] as Curve;
				e = Math.min(segLength + s, curve.length);
				subPath = curve.getCurveSegment(s, e);
				DrawUtils.moveGraphicPath(subPath, startPoint);
				pathData = DrawUtils.joinGraphicPaths(pathData, subPath);
				segLength -= e-s;
				if (segLength == 0)	break;
				if (s > 0)			s = 0;
				startPoint = startPoint.add(new Point(curve.anchorX, curve.anchorY));
			}
			return pathData;
		}
		
		/**
		 * @copy com.janisRadins.asDraw#getPointOnCurve()
		 */
		public function getPointOnCurve(position:Number):Point {
			var curvePos:CurveData = getCurveOnPosition(position);
			if (curvePos == null) {
				throw new Error("Curve for requested position [" + position + "] not found");
			}
			return curvePos.coordinates.add(curvePos.curve.getPointOnCurve(position - curvePos.startLength));
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurve#getAngleAtPoint()
		 */
		public function getAngleAtPoint(position:Number):Number {
			var curvePos:CurveData = getCurveOnPosition(position);
			return curvePos.curve.getAngleAtPoint(position - curvePos.startLength);
		}
		/**
		 * @copy com.janisRadins.asDraw#getCurveSegment()
		 */
		public function getCurveSegment(start:Number = NaN, end:Number = NaN):GraphicsPath {
			if (isNaN(start))	start = 0;
			if (isNaN(end))		end = length;
			if (start == end)	return new GraphicsPath();
			if (start < end)	return getSubSegment(start, end);
			else				return DrawUtils.joinGraphicPaths(getSubSegment(start, length), getSubSegment(0, end));
			return null;
		}
		/**
		 * Method that returns Curve instance which belongs to specific position value of CurveCollection.
		 * @param	position curve length value for which corresponding curve should be found
		 * @return	Curve instance which is holding requested point
		 */
		private function getCurveOnPosition(position:Number):CurveData {
			var startPoint:Point = new Point(_x, _y);
			var startLength:Number = 0;
			var curve:Curve;
			for (var i:uint = 0; i < curveList.length; i++) {
				curve = curveList[i];
				if (position >= startLength && position <= startLength + curve.length) {
					return new CurveData(startPoint, startLength, curve, i);
				}
				startPoint.x += curve.anchorX;
				startPoint.y += curve.anchorY;
				startLength += curve.length;
			}
			return null;
		}
		
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#curveCount
		 */
		public function get curveCount():uint {
			return curveList.length;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#curveIndex
		 */
		public function get curveIndex():uint {
			return _curveIndex;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#getCurveAt()
		 */
		public function getCurveAt(index:int):ICurve {
			if (index < 0) {
				index += curveList.length;
			}
			if (index >= curveList.length) {
				throw new Error("Curve index [" + index + "] out of range", ERROR_OOR);
			}
			return curveList[index];
		}
		
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#setCurveAt()
		 */
		public function setCurveAt(curve:Curve, index:int):void {
			if (index < 0) {
				index += curveList.length;
			}
			if (index >= curveList.length) {
				throw new Error("Curve index [" + index + "] out of range", ERROR_OOR);
			}
			curveList.splice(index, 0, [curve]);
			dispatchEvent(new Event(COLLECTION_CHANGED));
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#changeCurveIndex()
		 */
		public function changeCurveIndex(oldIndex:int, newIndex:int):void {
			if (oldIndex < 0)	oldIndex += curveList.length;
			if (newIndex < 0)	newIndex += curveList.length;
			
			if (oldIndex >= curveList.length || newIndex >= curveList.length) {
				throw new Error("Some index out of range", ERROR_OOR);
			}
			curveList.splice(newIndex, 0, curveList.splice(oldIndex, 1));
			dispatchEvent(new Event(COLLECTION_CHANGED));
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#removeCurve()
		 */
		public function removeCurve(curve:Curve):ICurve {
			if (contains(curve) == false) {
				throw new Error("Curve not found", ERROR_CNF)
			}
			while (true) {
				var index:int = curveList.indexOf(curve);
				if (index == -1) {
					break;
				}
				curveList.splice(index, 1);
			}
			if (contains(curve) == false) {
				curve.removeEventListener(CurveChangeEvent.CHANGE, onCurveValueChange);
			}
			dispatchEvent(new Event(COLLECTION_CHANGED));
			return curve;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#removeCurveAt()
		 */
		public function removeCurveAt(index:int):ICurve {
			if (index < 0) {
				index += curveList.length;
			}
			if (index >= curveList.length) {
				throw new Error("Curve index [" + index + "] out of range", ERROR_OOR);
			}
			var curve:Curve = curveList.splice(index, 1)[0];
			if (contains(curve) == false) {
				curve.removeEventListener(CurveChangeEvent.CHANGE, onCurveValueChange);
			}
			dispatchEvent(new Event(COLLECTION_CHANGED));
			return curve;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#contains()
		 */
		public function contains(curve:Curve):Boolean {
			return (curveList.indexOf(curve) != -1);
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#getCurveData()
		 */
		public function getCurveData(curve:ICurve):CurveData {
			var startPoint:Point = new Point(_x, _y);
			var startLength:Number = 0;
			var subCurve:Curve;
			for (var i:uint = 0; i < curveList.length; i++) {
				subCurve = curveList[i];
				if (curve == subCurve) {
					return new CurveData(startPoint, startLength, subCurve, i);
				}
				startPoint.x += subCurve.anchorX;
				startPoint.y += subCurve.anchorY;
				startLength += subCurve.length;
			}
			return null;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#hasNext
		 */
		public function get hasNext():Boolean {
			return _curveIndex < curveList.length - 1;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#getNext()
		 */
		public function getNext():ICurve {
			if (hasNext == false) {
				return null;
			}
			return curveList[_curveIndex++];
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#hasPrev
		 */
		public function get hasPrev():Boolean {
			return _curveIndex > 0;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#getPrev()
		 */
		public function getPrev():ICurve {
			if (hasPrev == false) {
				return null;
			}
			return curveList[_curveIndex--];
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#resetToStart()
		 */
		public function resetToStart():void {
			_curveIndex = 0;
		}
		/**
		 * @copy com.janisRadins.asDraw.ICurveCollection#resetToEnd()
		 */
		public function resetToEnd():void {
			// wtf am I doing in here?
			if (curveList.length > 0) {
				resetToStart();
			} else {
				_curveIndex = curveList.length-1;
			}
		}
		/**
		 * Method that creates exact clone of current Path
		 * @return new Path instance
		 */
		public function clone():Path {
			var newCurves:Vector.<Curve> = new Vector.<Curve>();
			for (var i:uint = 0; i < curveList.length; i++) {
				newCurves.push(Curve(curveList[i]).clone());
			}
			return new Path(newCurves);
		}
	}
}